package com.github.eclipseace.mybatis.spring;

import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Vector;
import java.util.concurrent.ConcurrentHashMap;

import javax.persistence.Entity;

import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.core.type.filter.TypeFilter;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;

/**
 * @author eclipseAce
 */
public class ClassPathEntityStore implements EntityStore, InitializingBean, ResourceLoaderAware {
	protected final Log logger = LogFactory.getLog(getClass());

	private ResourceLoader resourceLoader;
	private String[] basePackages;

	private final List<String> names = new Vector<>();
	private final Map<String, String> typeNames = new ConcurrentHashMap<>();
	private final Map<String, Class<?>> resolvedTypes = new ConcurrentHashMap<>();

	protected List<? extends TypeFilter> getTypeFilters() {
		return Collections.singletonList(new AnnotationTypeFilter(Entity.class));
	}

	protected String getEntityNameFromAnnotation(AnnotationMetadata anMetadata) {
		Map<String, Object> attrs = anMetadata.getAnnotationAttributes(Entity.class.getName());
		if (attrs != null) {
			return (String) attrs.get("name");
		}
		return null;
	}

	protected void doScanBasePackages() {
		ClassPathScanner scanner = new ClassPathScanner(resourceLoader, null);
		scanner.addTypeFilters(getTypeFilters());
		for (String pkg : basePackages) {
			if (logger.isDebugEnabled()) {
				logger.debug("Start to scan package for entities [" + pkg + "]");
			}
			try {
				for (MetadataReader metadataReader : scanner.scan(pkg)) {
					String clsName = metadataReader.getClassMetadata().getClassName();
					String name = getEntityNameFromAnnotation(metadataReader.getAnnotationMetadata());
					if (name.isEmpty()) {
						name = ClassUtils.getShortName(clsName).toLowerCase();
					}
					names.add(name);
					typeNames.put(name, clsName);
					
					if (logger.isDebugEnabled()) {
						logger.debug("Entity found: name=[" + name + "], class=[" + clsName + "]");
					}
				}
			} catch (IOException e) {
				throw new EntityStoreException("I/O failure during scanning package [" + pkg + "]", e);
			}
		}
	}

	@Override
	public void afterPropertiesSet() throws Exception {
		Assert.notEmpty(basePackages, "Property 'basePackages' is required");

		doScanBasePackages();
	}

	@Override
	public String[] getNames() throws EntityStoreException {
		return names.toArray(new String[names.size()]);
	}

	@Override
	public boolean contains(String name) throws EntityStoreException {
		return typeNames.containsKey(name);
	}

	@Override
	public String getTypeName(String name) throws EntityStoreException {
		return typeNames.get(name);
	}

	@Override
	public Class<?> getResolvedType(String name, ClassLoader clsLoader) throws EntityStoreException {
		Class<?> resolvedCls = resolvedTypes.get(name);
		if (resolvedCls == null) {
			String clsName = getTypeName(name);
			if (clsName != null) {
				try {
					resolvedCls = Class.forName(clsName, true,
							clsLoader != null ? clsLoader : getClass().getClassLoader());
				} catch (Throwable e) {
					throw new EntityStoreException("Failed to resolve entity class [" + clsName + "]", e);
				}
				resolvedTypes.put(clsName, resolvedCls);
			}
		}
		return resolvedCls;
	}

	@Override
	public void setResourceLoader(ResourceLoader resourceLoader) {
		this.resourceLoader = resourceLoader;
	}

	public void setBasePackages(String[] basePackages) {
		this.basePackages = basePackages;
	}
}
